import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.for-each.js";
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
import { addClass, removeClass } from "../../helpers/dom/element.mjs";
import { isNumeric, clamp } from "../../helpers/number.mjs";
import { toSingleLine } from "../../helpers/templateLiteralTag.mjs";
import { isLeftClick, isRightClick, isTouchEvent } from "../../helpers/dom/event.mjs";
import { warn } from "../../helpers/console.mjs";
import { ACTIVE_HEADER_TYPE, HEADER_TYPE } from "../../selection/index.mjs";
import { BasePlugin } from "../base/index.mjs";
import StateManager from "./stateManager/index.mjs";
import GhostTable from "./utils/ghostTable.mjs";
export const PLUGIN_KEY = 'nestedHeaders';
export const PLUGIN_PRIORITY = 280;

/* eslint-disable jsdoc/require-description-complete-sentence */

/**
 * @plugin NestedHeaders
 * @class NestedHeaders
 *
 * @description
 * The plugin allows to create a nested header structure, using the HTML's colspan attribute.
 *
 * To make any header wider (covering multiple table columns), it's corresponding configuration array element should be
 * provided as an object with `label` and `colspan` properties. The `label` property defines the header's label,
 * while the `colspan` property defines a number of columns that the header should cover.
 * You can also set custom class names to any of the headers by providing the `headerClassName` property.
 *
 * __Note__ that the plugin supports a *nested* structure, which means, any header cannot be wider than it's "parent". In
 * other words, headers cannot overlap each other.
 * @example
 *
 * ::: only-for javascript
 * ```js
 * const container = document.getElementById('example');
 * const hot = new Handsontable(container, {
 *   data: getData(),
 *   nestedHeaders: [
 *     ['A', {label: 'B', colspan: 8, headerClassName: 'htRight'}, 'C'],
 *     ['D', {label: 'E', colspan: 4}, {label: 'F', colspan: 4}, 'G'],
 *     ['H', {label: 'I', colspan: 2}, {label: 'J', colspan: 2}, {label: 'K', colspan: 2}, {label: 'L', colspan: 2}, 'M'],
 *     ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
 *  ],
 * ```
 * :::
 *
 * ::: only-for react
 * ```jsx
 * <HotTable
 *   data={getData()}
 *   nestedHeaders={[
 *     ['A', {label: 'B', colspan: 8, headerClassName: 'htRight'}, 'C'],
 *     ['D', {label: 'E', colspan: 4}, {label: 'F', colspan: 4}, 'G'],
 *     ['H', {label: 'I', colspan: 2}, {label: 'J', colspan: 2}, {label: 'K', colspan: 2}, {label: 'L', colspan: 2}, 'M'],
 *     ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
 *  ]}
 * />
 * ```
 * :::
 *
 * ::: only-for angular
 * ```ts
 * settings = {
 *   data: getData(),
 *   nestedHeaders: [
 *     ["A", { label: "B", colspan: 8, headerClassName: "htRight" }, "C"],
 *     ["D", { label: "E", colspan: 4 }, { label: "F", colspan: 4 }, "G"],
 *     [
 *       "H",
 *       { label: "I", colspan: 2 },
 *       { label: "J", colspan: 2 },
 *       { label: "K", colspan: 2 },
 *       { label: "L", colspan: 2 },
 *       "M",
 *     ],
 *     ["N", "O", "P", "Q", "R", "S", "T", "U", "V", "W"],
 *   ],
 * };
 * ```
 *
 * ```html
 * <hot-table [settings]="settings"></hot-table>
 * ```
 * :::
 */
var _stateManager = /*#__PURE__*/new WeakMap();
var _hidingIndexMapObserver = /*#__PURE__*/new WeakMap();
var _focusInitialCoords = /*#__PURE__*/new WeakMap();
var _isColumnsSelectionInProgress = /*#__PURE__*/new WeakMap();
var _recentlyHighlightCoords = /*#__PURE__*/new WeakMap();
var _updateFocusHighlightPosition = /*#__PURE__*/new WeakMap();
var _NestedHeaders_brand = /*#__PURE__*/new WeakSet();
export class NestedHeaders extends BasePlugin {
  constructor() {
    super(...arguments);
    /**
     * Allows to control to which column index the viewport will be scrolled. To ensure that the viewport
     * is scrolled to the correct column for the nested header the most left and the most right visual column
     * indexes are used.
     *
     * @param {number} visualColumn A visual column index to which the viewport will be scrolled.
     * @param {{ value: 'auto' | 'start' | 'end' }} snapping If `'start'`, viewport is scrolled to show
     * the cell on the left of the table. If `'end'`, viewport is scrolled to show the cell on the right of
     * the table. When `'auto'`, the viewport is scrolled only when the column is outside of the viewport.
     * @returns {number}
     */
    _classPrivateMethodInitSpec(this, _NestedHeaders_brand);
    /**
     * The state manager for the nested headers.
     *
     * @type {StateManager}
     */
    _classPrivateFieldInitSpec(this, _stateManager, new StateManager());
    /**
     * The instance of the ChangesObservable class that allows track the changes that happens in the
     * column indexes.
     *
     * @type {ChangesObservable}
     */
    _classPrivateFieldInitSpec(this, _hidingIndexMapObserver, null);
    /**
     * Holds the coords that points to the place where the column selection starts.
     *
     * @type {number|null}
     */
    _classPrivateFieldInitSpec(this, _focusInitialCoords, null);
    /**
     * Determines if there is performed the column selection.
     *
     * @type {boolean}
     */
    _classPrivateFieldInitSpec(this, _isColumnsSelectionInProgress, false);
    /**
     * Keeps the last highlight position made by column selection. The coords are necessary to scroll
     * the viewport to the correct position when the nested header is clicked when the `navigableHeaders`
     * option is disabled.
     *
     * @type {CellCoords | null}
     */
    _classPrivateFieldInitSpec(this, _recentlyHighlightCoords, null);
    /**
     * Custom helper for getting widths of the nested headers.
     *
     * @private
     * @type {GhostTable}
     */
    // @TODO This should be changed after refactor handsontable/utils/ghostTable.
    _defineProperty(this, "ghostTable", new GhostTable(this.hot, (row, column) => this.getHeaderSettings(row, column)));
    /**
     * The flag which determines that the nested header settings contains overlapping headers
     * configuration.
     *
     * @type {boolean}
     */
    _defineProperty(this, "detectedOverlappedHeaders", false);
    /**
     * Updates the selection focus highlight position to point to the nested header root element (TH)
     * even when the logical coordinates point in-between the header.
     *
     * The method uses arrow function to keep the reference to the class method. Necessary for
     * the `removeLocalHook` method of the row and column index mapper.
     */
    _classPrivateFieldInitSpec(this, _updateFocusHighlightPosition, () => {
      var _this$hot;
      const selection = (_this$hot = this.hot) === null || _this$hot === void 0 ? void 0 : _this$hot.getSelectedRangeActive();
      if (!selection) {
        return;
      }
      const {
        highlight
      } = selection;
      const isNestedHeadersRange = highlight.isHeader() && highlight.col >= 0;
      if (isNestedHeadersRange) {
        const columnIndex = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(highlight.row, highlight.col);
        const focusHighlight = this.hot.selection.highlight.getFocus();

        // Correct the highlight/focus selection to highlight the correct TH element
        focusHighlight.visualCellRange.highlight.col = columnIndex;
        focusHighlight.visualCellRange.from.col = columnIndex;
        focusHighlight.visualCellRange.to.col = columnIndex;
        focusHighlight.commit();
      }
    });
  }
  static get PLUGIN_KEY() {
    return PLUGIN_KEY;
  }
  static get PLUGIN_PRIORITY() {
    return PLUGIN_PRIORITY;
  }
  /**
   * Check if plugin is enabled.
   *
   * @returns {boolean}
   */
  isEnabled() {
    return !!this.hot.getSettings()[PLUGIN_KEY];
  }

  /**
   * Enables the plugin functionality for this Handsontable instance.
   */
  enablePlugin() {
    var _this = this;
    if (this.enabled) {
      return;
    }
    const {
      nestedHeaders
    } = this.hot.getSettings();
    if (!Array.isArray(nestedHeaders) || !Array.isArray(nestedHeaders[0])) {
      warn(toSingleLine`Your Nested Headers plugin configuration is invalid. The settings has to be\x20
                        passed as an array of arrays e.q. [['A1', { label: 'A2', colspan: 2 }]]`);
    }
    this.addHook('init', () => _assertClassBrand(_NestedHeaders_brand, this, _onInit).call(this));
    this.addHook('afterLoadData', function () {
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterLoadData).call(_this, ...args);
    });
    this.addHook('beforeOnCellMouseDown', function () {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseDown).call(_this, ...args);
    });
    this.addHook('afterOnCellMouseDown', function () {
      for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
        args[_key3] = arguments[_key3];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterOnCellMouseDown).call(_this, ...args);
    });
    this.addHook('beforeOnCellMouseOver', function () {
      for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
        args[_key4] = arguments[_key4];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseOver).call(_this, ...args);
    });
    this.addHook('beforeOnCellMouseUp', function () {
      for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
        args[_key5] = arguments[_key5];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseUp).call(_this, ...args);
    });
    this.addHook('beforeSelectionHighlightSet', function () {
      for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
        args[_key6] = arguments[_key6];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeSelectionHighlightSet).call(_this, ...args);
    });
    this.addHook('modifyTransformStart', function () {
      for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
        args[_key7] = arguments[_key7];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyTransformStart).call(_this, ...args);
    });
    this.addHook('afterSelection', () => _classPrivateFieldGet(_updateFocusHighlightPosition, this).call(this));
    this.addHook('afterSelectionFocusSet', () => _classPrivateFieldGet(_updateFocusHighlightPosition, this).call(this));
    this.addHook('beforeViewportScrollHorizontally', function () {
      for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
        args[_key8] = arguments[_key8];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeViewportScrollHorizontally).call(_this, ...args);
    });
    this.addHook('afterGetColumnHeaderRenderers', array => _assertClassBrand(_NestedHeaders_brand, this, _onAfterGetColumnHeaderRenderers).call(this, array));
    this.addHook('modifyColWidth', function () {
      for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
        args[_key9] = arguments[_key9];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyColWidth).call(_this, ...args);
    });
    this.addHook('modifyColumnHeaderValue', function () {
      for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) {
        args[_key0] = arguments[_key0];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyColumnHeaderValue).call(_this, ...args);
    });
    this.addHook('beforeHighlightingColumnHeader', function () {
      for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) {
        args[_key1] = arguments[_key1];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeHighlightingColumnHeader).call(_this, ...args);
    });
    this.addHook('beforeCopy', function () {
      for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
        args[_key10] = arguments[_key10];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeCopy).call(_this, ...args);
    });
    this.addHook('beforeSelectColumns', function () {
      for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
        args[_key11] = arguments[_key11];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeSelectColumns).call(_this, ...args);
    });
    this.addHook('afterViewportColumnCalculatorOverride', function () {
      for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
        args[_key12] = arguments[_key12];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterViewportColumnCalculatorOverride).call(_this, ...args);
    });
    this.addHook('modifyFocusedElement', function () {
      for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
        args[_key13] = arguments[_key13];
      }
      return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyFocusedElement).call(_this, ...args);
    });
    this.hot.columnIndexMapper.addLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this));
    this.hot.rowIndexMapper.addLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this));
    super.enablePlugin();
    this.updatePlugin(); // @TODO: Workaround for broken plugin initialization abstraction.
  }

  /**
   * Updates the plugin's state.
   *
   * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options:
   *  - [`nestedHeaders`](@/api/options.md#nestedheaders)
   */
  updatePlugin() {
    if (!this.hot.view) {
      // @TODO: Workaround for broken plugin initialization abstraction.
      return;
    }
    const {
      nestedHeaders
    } = this.hot.getSettings();
    _classPrivateFieldGet(_stateManager, this).setColumnsLimit(this.hot.countCols());
    if (Array.isArray(nestedHeaders)) {
      this.detectedOverlappedHeaders = _classPrivateFieldGet(_stateManager, this).setState(nestedHeaders);
    }
    if (this.detectedOverlappedHeaders) {
      warn(toSingleLine`Your Nested Headers plugin setup contains overlapping headers. This kind of configuration\x20
                        is currently not supported.`);
    }
    if (this.enabled) {
      // This line covers the case when a developer uses the external hiding maps to manipulate
      // the columns' visibility. The tree state built from the settings - which is always built
      // as if all the columns are visible, needs to be modified to be in sync with a dataset.
      this.hot.columnIndexMapper.hidingMapsCollection.getMergedValues().forEach((isColumnHidden, physicalColumnIndex) => {
        const actionName = isColumnHidden === true ? 'hide-column' : 'show-column';
        _classPrivateFieldGet(_stateManager, this).triggerColumnModification(actionName, physicalColumnIndex);
      });
    }
    if (!_classPrivateFieldGet(_hidingIndexMapObserver, this) && this.enabled) {
      _classPrivateFieldSet(_hidingIndexMapObserver, this, this.hot.columnIndexMapper.createChangesObserver('hiding').subscribe(changes => {
        changes.forEach(_ref => {
          let {
            op,
            index: columnIndex,
            newValue
          } = _ref;
          if (op === 'replace') {
            const actionName = newValue === true ? 'hide-column' : 'show-column';
            _classPrivateFieldGet(_stateManager, this).triggerColumnModification(actionName, columnIndex);
          }
        });
        this.ghostTable.buildWidthsMap();
      }));
    }
    this.ghostTable.setLayersCount(this.getLayersCount()).buildWidthsMap();
    super.updatePlugin();
  }

  /**
   * Disables the plugin functionality for this Handsontable instance.
   */
  disablePlugin() {
    this.hot.rowIndexMapper.removeLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this));
    this.hot.columnIndexMapper.removeLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this));
    this.clearColspans();
    _classPrivateFieldGet(_stateManager, this).clear();
    _classPrivateFieldGet(_hidingIndexMapObserver, this).unsubscribe();
    _classPrivateFieldSet(_hidingIndexMapObserver, this, null);
    this.ghostTable.clear();
    super.disablePlugin();
  }

  /**
   * Returns an instance of the internal state manager of the plugin.
   *
   * @private
   * @returns {StateManager}
   */
  getStateManager() {
    return _classPrivateFieldGet(_stateManager, this);
  }

  /**
   * Gets a total number of headers levels.
   *
   * @private
   * @returns {number}
   */
  getLayersCount() {
    return _classPrivateFieldGet(_stateManager, this).getLayersCount();
  }

  /**
   * Gets column settings for a specified header. The returned object contains
   * information about the header label, its colspan length, or if it is hidden
   * in the header renderers.
   *
   * @private
   * @param {number} headerLevel Header level (0 = most distant to the table).
   * @param {number} columnIndex A visual column index.
   * @returns {object}
   */
  getHeaderSettings(headerLevel, columnIndex) {
    return _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, columnIndex);
  }

  /**
   * Clear the colspans remaining after plugin usage.
   *
   * @private
   */
  clearColspans() {
    if (!this.hot.view) {
      return;
    }
    const {
      _wt: wt
    } = this.hot.view;
    const headerLevels = wt.getSetting('columnHeaders').length;
    const mainHeaders = wt.wtTable.THEAD;
    const topHeaders = wt.wtOverlays.topOverlay.clone.wtTable.THEAD;
    const topLeftCornerHeaders = wt.wtOverlays.topInlineStartCornerOverlay ? wt.wtOverlays.topInlineStartCornerOverlay.clone.wtTable.THEAD : null;
    for (let i = 0; i < headerLevels; i++) {
      const masterLevel = mainHeaders.childNodes[i];
      if (!masterLevel) {
        break;
      }
      const topLevel = topHeaders.childNodes[i];
      const topLeftCornerLevel = topLeftCornerHeaders ? topLeftCornerHeaders.childNodes[i] : null;
      for (let j = 0, masterNodes = masterLevel.childNodes.length; j < masterNodes; j++) {
        masterLevel.childNodes[j].removeAttribute('colspan');
        removeClass(masterLevel.childNodes[j], 'hiddenHeader');
        if (topLevel && topLevel.childNodes[j]) {
          topLevel.childNodes[j].removeAttribute('colspan');
          removeClass(topLevel.childNodes[j], 'hiddenHeader');
        }
        if (topLeftCornerHeaders && topLeftCornerLevel && topLeftCornerLevel.childNodes[j]) {
          topLeftCornerLevel.childNodes[j].removeAttribute('colspan');
          removeClass(topLeftCornerLevel.childNodes[j], 'hiddenHeader');
        }
      }
    }
  }

  /**
   * Generates the appropriate header renderer for a header row.
   *
   * @private
   * @param {number} headerLevel The index of header level counting from the top (positive
   *                             values counting from 0 to N).
   * @returns {Function}
   * @fires Hooks#afterGetColHeader
   */
  headerRendererFactory(headerLevel) {
    var _this2 = this;
    const fixedColumnsStart = this.hot.view._wt.getSetting('fixedColumnsStart');
    return (renderedColumnIndex, TH) => {
      var _classPrivateFieldGet2;
      const {
        columnIndexMapper,
        view
      } = this.hot;
      let visualColumnIndex = columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex);
      if (visualColumnIndex === null) {
        visualColumnIndex = renderedColumnIndex;
      }
      TH.removeAttribute('colspan');
      removeClass(TH, 'hiddenHeader');
      removeClass(TH, 'hiddenHeaderText');
      const {
        colspan,
        isHidden,
        isPlaceholder,
        headerClassNames
      } = (_classPrivateFieldGet2 = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet2 !== void 0 ? _classPrivateFieldGet2 : {
        label: ''
      };
      if (isPlaceholder || isHidden) {
        addClass(TH, 'hiddenHeader');
      } else if (colspan > 1) {
        var _wtOverlays$topInline, _wtOverlays$inlineSta, _wtOverlays$topOverla;
        const {
          wtOverlays
        } = view._wt;
        const isTopInlineStartOverlay = (_wtOverlays$topInline = wtOverlays.topInlineStartCornerOverlay) === null || _wtOverlays$topInline === void 0 ? void 0 : _wtOverlays$topInline.clone.wtTable.THEAD.contains(TH);
        const isInlineStartOverlay = (_wtOverlays$inlineSta = wtOverlays.inlineStartOverlay) === null || _wtOverlays$inlineSta === void 0 ? void 0 : _wtOverlays$inlineSta.clone.wtTable.THEAD.contains(TH);
        const isTopOverlay = (_wtOverlays$topOverla = wtOverlays.topOverlay) === null || _wtOverlays$topOverla === void 0 ? void 0 : _wtOverlays$topOverla.clone.wtTable.THEAD.contains(TH);
        if (isTopOverlay && visualColumnIndex < fixedColumnsStart) {
          addClass(TH, 'hiddenHeaderText');
        }

        // Check if there is a fixed column enabled, if so then reduce colspan to fixed column width.
        const correctedColspan = isTopInlineStartOverlay || isInlineStartOverlay ? Math.min(colspan, fixedColumnsStart - renderedColumnIndex) : colspan;
        if (correctedColspan > 1) {
          TH.setAttribute('colspan', correctedColspan);
        }
      }
      this.hot.view.appendColHeader(visualColumnIndex, TH, function () {
        return _this2.getColumnHeaderValue(...arguments);
      }, headerLevel);

      // Replace the higher-order `headerClassName`s with the one provided in the plugin config, if it was provided.
      if (!isPlaceholder && !isHidden) {
        const innerHeaderDiv = TH.querySelector('div.relative');
        if (innerHeaderDiv && headerClassNames && headerClassNames.length > 0) {
          removeClass(innerHeaderDiv, this.hot.getColumnMeta(visualColumnIndex).headerClassName);
          addClass(innerHeaderDiv, headerClassNames);
        }
      }
    };
  }

  /**
   * Returns the column header value for specified column and header level index.
   *
   * @private
   * @param {number} visualColumnIndex Visual column index.
   * @param {number} headerLevel The index of header level. The header level accepts positive (0 to N)
   *                             and negative (-1 to -N) values. For positive values, 0 points to the
   *                             top most header, and for negative direction, -1 points to the most bottom
   *                             header (the header closest to the cells).
   * @returns {string} Returns the column header value to update.
   */
  getColumnHeaderValue(visualColumnIndex, headerLevel) {
    var _classPrivateFieldGet3;
    const {
      isHidden,
      isPlaceholder
    } = (_classPrivateFieldGet3 = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet3 !== void 0 ? _classPrivateFieldGet3 : {};
    if (isPlaceholder || isHidden) {
      return '';
    }
    return this.hot.getColHeader(visualColumnIndex, headerLevel);
  }
  /**
   * Destroys the plugin instance.
   */
  destroy() {
    _classPrivateFieldSet(_stateManager, this, null);
    if (_classPrivateFieldGet(_hidingIndexMapObserver, this) !== null) {
      _classPrivateFieldGet(_hidingIndexMapObserver, this).unsubscribe();
      _classPrivateFieldSet(_hidingIndexMapObserver, this, null);
    }
    super.destroy();
  }

  /**
   * Gets the tree data that belongs to the column headers pointed by the passed coordinates.
   *
   * @private
   * @param {CellCoords} coords The CellCoords instance.
   * @returns {object|undefined}
   */
  _getHeaderTreeNodeDataByCoords(coords) {
    if (coords.row >= 0 || coords.col < 0) {
      return;
    }
    return _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(coords.row, coords.col);
  }
}
function _onBeforeViewportScrollHorizontally(visualColumn, snapping) {
  var _classPrivateFieldGet4;
  const selection = this.hot.getSelectedRangeActive();
  if (!selection) {
    return visualColumn;
  }
  const {
    highlight
  } = selection;
  const {
    navigableHeaders
  } = this.hot.getSettings();
  const isSelectedByColumnHeader = this.hot.selection.isSelectedByColumnHeader();
  const highlightRow = navigableHeaders ? highlight.row : (_classPrivateFieldGet4 = _classPrivateFieldGet(_recentlyHighlightCoords, this)) === null || _classPrivateFieldGet4 === void 0 ? void 0 : _classPrivateFieldGet4.row;
  const highlightColumn = isSelectedByColumnHeader ? visualColumn : highlight.col;
  const isNestedHeadersRange = highlightRow < 0 && highlightColumn >= 0;
  _classPrivateFieldSet(_recentlyHighlightCoords, this, null);
  if (!isNestedHeadersRange) {
    return visualColumn;
  }
  const firstVisibleColumn = this.hot.getFirstFullyVisibleColumn();
  const lastVisibleColumn = this.hot.getLastFullyVisibleColumn();
  const viewportWidth = lastVisibleColumn - firstVisibleColumn + 1;
  const mostLeftColumnIndex = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(highlightRow, highlightColumn);
  const mostRightColumnIndex = _classPrivateFieldGet(_stateManager, this).findRightMostColumnIndex(highlightRow, highlightColumn);
  const headerWidth = mostRightColumnIndex - mostLeftColumnIndex + 1;

  // scroll the viewport always to the left when the header is wider than the viewport
  if (mostLeftColumnIndex < firstVisibleColumn && mostRightColumnIndex > lastVisibleColumn) {
    return mostLeftColumnIndex;
  }
  if (isSelectedByColumnHeader) {
    let scrollColumnIndex = null;
    if (mostLeftColumnIndex >= firstVisibleColumn && mostRightColumnIndex > lastVisibleColumn) {
      if (headerWidth > viewportWidth) {
        snapping.value = 'start';
        scrollColumnIndex = mostLeftColumnIndex;
      } else {
        snapping.value = 'end';
        scrollColumnIndex = mostRightColumnIndex;
      }
    } else if (mostLeftColumnIndex < firstVisibleColumn && mostRightColumnIndex <= lastVisibleColumn) {
      if (headerWidth > viewportWidth) {
        snapping.value = 'end';
        scrollColumnIndex = mostRightColumnIndex;
      } else {
        snapping.value = 'start';
        scrollColumnIndex = mostLeftColumnIndex;
      }
    }
    return scrollColumnIndex;
  }
  return mostLeftColumnIndex <= firstVisibleColumn ? mostLeftColumnIndex : mostRightColumnIndex;
}
/**
 * Allows to control which header DOM element will be used to highlight.
 *
 * @param {number} visualColumn A visual column index of the highlighted row header.
 * @param {number} headerLevel A row header level that is currently highlighted.
 * @param {object} highlightMeta An object with meta data that describes the highlight state.
 * @returns {number}
 */
function _onBeforeHighlightingColumnHeader(visualColumn, headerLevel, highlightMeta) {
  const headerNodeData = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(headerLevel, visualColumn);
  if (!headerNodeData) {
    return visualColumn;
  }
  const {
    columnCursor,
    selectionType,
    selectionWidth
  } = highlightMeta;
  const {
    isRoot,
    colspan
  } = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumn);
  if (selectionType === HEADER_TYPE) {
    if (!isRoot) {
      return headerNodeData.columnIndex;
    }
  } else if (selectionType === ACTIVE_HEADER_TYPE) {
    if (colspan > selectionWidth - columnCursor || !isRoot) {
      // Prevents adding any CSS class names to the TH element
      return null;
    }
  }
  return visualColumn;
}
/**
 * Listens the `beforeCopy` hook that allows processing the copied column headers so that the
 * merged column headers do not propagate the value for each column but only once at the beginning
 * of the column.
 *
 * @private
 * @param {Array[]} data An array of arrays which contains data to copied.
 * @param {object[]} copyableRanges An array of objects with ranges of the visual indexes (`startRow`, `startCol`, `endRow`, `endCol`)
 *                                  which will copied.
 * @param {{ columnHeadersCount: number }} copiedHeadersCount An object with keys that holds information with
 *                                                            the number of copied headers.
 */
function _onBeforeCopy(data, copyableRanges, _ref2) {
  let {
    columnHeadersCount
  } = _ref2;
  if (columnHeadersCount === 0) {
    return;
  }
  for (let rangeIndex = 0; rangeIndex < copyableRanges.length; rangeIndex++) {
    const {
      startRow,
      startCol,
      endRow,
      endCol
    } = copyableRanges[rangeIndex];
    const rowsCount = endRow - startRow + 1;
    const columnsCount = startCol - endCol + 1;

    // do not process dataset ranges and column headers where only one column is copied
    if (startRow >= 0 || columnsCount === 1) {
      break;
    }
    for (let column = startCol; column <= endCol; column++) {
      for (let row = startRow; row <= endRow; row++) {
        var _classPrivateFieldGet5;
        const zeroBasedColumnHeaderLevel = rowsCount + row;
        const zeroBasedColumnIndex = column - startCol;
        if (zeroBasedColumnIndex === 0) {
          continue; // eslint-disable-line no-continue
        }
        const isRoot = (_classPrivateFieldGet5 = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(row, column)) === null || _classPrivateFieldGet5 === void 0 ? void 0 : _classPrivateFieldGet5.isRoot;
        if (isRoot === false) {
          data[zeroBasedColumnHeaderLevel][zeroBasedColumnIndex] = '';
        }
      }
    }
  }
}
/**
 * Allows blocking the column selection that is controlled by the core Selection module.
 *
 * @param {MouseEvent} event Mouse event.
 * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell.
 * @param {CellCoords} TD The table cell or header element.
 * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains
 *                            a boolean value that allows or disallows changing the selection for that particular area.
 */
function _onBeforeOnCellMouseDown(event, coords, TD, controller) {
  const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords);
  if (headerNodeData) {
    // Block the Selection module in controlling how the columns are selected. Pass the
    // responsibility of the column selection to this plugin (see "onAfterOnCellMouseDown" hook).
    controller.column = true;
  }
}
/**
 * Allows to control how the column selection based on the coordinates and the nested headers is made.
 *
 * @param {MouseEvent} event Mouse event.
 * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell.
 */
function _onAfterOnCellMouseDown(event, coords) {
  const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords);
  if (!headerNodeData) {
    return;
  }
  _classPrivateFieldSet(_focusInitialCoords, this, coords.clone());
  _classPrivateFieldSet(_isColumnsSelectionInProgress, this, true);
  const {
    selection
  } = this.hot;
  const currentSelection = selection.isSelected() ? selection.getSelectedRange().current() : null;
  const columnsToSelect = [];
  const {
    columnIndex,
    origColspan
  } = headerNodeData;

  // The Selection module doesn't allow it to extend its behavior easily. That's why here we need
  // to re-implement the "click" and "shift" behavior. As a workaround, the logic for the nested
  // headers must implement a similar logic as in the original Selection handler
  // (see src/selection/mouseEventHandler.js).
  const allowRightClickSelection = !selection.inInSelection(coords);
  if (event.shiftKey && currentSelection) {
    if (coords.col < currentSelection.from.col) {
      columnsToSelect.push(currentSelection.getTopEndCorner().col, columnIndex, coords.row);
    } else if (coords.col > currentSelection.from.col) {
      columnsToSelect.push(currentSelection.getTopStartCorner().col, columnIndex + origColspan - 1, coords.row);
    } else {
      columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, coords.row);
    }
  } else if (isLeftClick(event) || isRightClick(event) && allowRightClickSelection || isTouchEvent(event)) {
    columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, coords.row);
  }

  // The plugin takes control of how the columns are selected.
  selection.selectColumns(...columnsToSelect);
}
/**
 * Makes the header-selection properly select the nested headers.
 *
 * @param {MouseEvent} event Mouse event.
 * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell.
 * @param {HTMLElement} TD The cell element.
 * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains
 *                            a boolean value that allows or disallows changing the selection for that particular area.
 */
function _onBeforeOnCellMouseOver(event, coords, TD, controller) {
  if (!this.hot.view.isMouseDown() || controller.column) {
    return;
  }
  const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords);
  if (!headerNodeData) {
    return;
  }
  const {
    columnIndex,
    origColspan
  } = headerNodeData;
  const selectedRange = this.hot.getSelectedRangeActive();
  const topStartCoords = selectedRange.getTopStartCorner();
  const bottomEndCoords = selectedRange.getBottomEndCorner();
  const {
    from
  } = selectedRange;

  // Block the Selection module in controlling how the columns and cells are selected.
  // From now on, the plugin is responsible for the selection.
  controller.column = true;
  controller.cell = true;
  const columnsToSelect = [];
  const headerLevel = clamp(coords.row, -Infinity, -1);
  if (coords.col < from.col) {
    columnsToSelect.push(bottomEndCoords.col, columnIndex, headerLevel);
  } else if (coords.col > from.col) {
    columnsToSelect.push(topStartCoords.col, columnIndex + origColspan - 1, headerLevel);
  } else {
    columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, headerLevel);
  }
  this.hot.selection.selectColumns(...columnsToSelect);
}
/**
 * Switches internal flag about selection progress to `false`.
 */
function _onBeforeOnCellMouseUp() {
  _classPrivateFieldSet(_isColumnsSelectionInProgress, this, false);
}
/**
 * The hook checks and ensures that the focus position that depends on the selected columns
 * range is always positioned within the range.
 */
function _onBeforeSelectionHighlightSet() {
  const {
    navigableHeaders
  } = this.hot.getSettings();
  if (!this.hot.view.isMouseDown() || !_classPrivateFieldGet(_isColumnsSelectionInProgress, this) || !navigableHeaders) {
    return;
  }
  const selectedRange = this.hot.getSelectedRangeLast();
  const columnStart = selectedRange.getTopStartCorner().col;
  const columnEnd = selectedRange.getBottomEndCorner().col;
  const {
    columnIndex,
    origColspan
  } = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(_classPrivateFieldGet(_focusInitialCoords, this).row, _classPrivateFieldGet(_focusInitialCoords, this).col);
  selectedRange.setHighlight(_classPrivateFieldGet(_focusInitialCoords, this));
  if (origColspan > selectedRange.getWidth() || columnIndex < columnStart || columnIndex + origColspan - 1 > columnEnd) {
    const headerLevel = _classPrivateFieldGet(_stateManager, this).findTopMostEntireHeaderLevel(clamp(columnStart, columnIndex, columnIndex + origColspan - 1), clamp(columnEnd, columnIndex, columnIndex + origColspan - 1));
    selectedRange.highlight.row = headerLevel;
    selectedRange.highlight.col = selectedRange.from.col;
  }
}
/**
 * `modifyTransformStart` hook is called every time the keyboard navigation is used.
 *
 * @param {object} delta The transformation delta.
 */
function _onModifyTransformStart(delta) {
  const {
    highlight
  } = this.hot.getSelectedRangeActive();
  const nextCoords = this.hot._createCellCoords(highlight.row + delta.row, highlight.col + delta.col);
  const isNestedHeadersRange = nextCoords.isHeader() && nextCoords.col >= 0;
  if (!isNestedHeadersRange) {
    return;
  }
  const visualColumnIndexStart = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(nextCoords.row, nextCoords.col);
  const visualColumnIndexEnd = _classPrivateFieldGet(_stateManager, this).findRightMostColumnIndex(nextCoords.row, nextCoords.col);
  if (delta.col < 0) {
    const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexStart - 1 : visualColumnIndexEnd;
    const notHiddenColumnIndex = this.hot.columnIndexMapper.getNearestNotHiddenIndex(nextColumn, -1);
    if (notHiddenColumnIndex === null) {
      // There are no visible columns anymore, so move the selection out of the table edge. This will
      // be processed by the selection Transformer class as a move selection to the previous row (if autoWrapRow is enabled).
      delta.col = -this.hot.view.countRenderableColumnsInRange(0, highlight.col);
    } else {
      delta.col = -Math.max(this.hot.view.countRenderableColumnsInRange(notHiddenColumnIndex, highlight.col) - 1, 1);
    }
  } else if (delta.col > 0) {
    const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexEnd + 1 : visualColumnIndexStart;
    const notHiddenColumnIndex = this.hot.columnIndexMapper.getNearestNotHiddenIndex(nextColumn, 1);
    if (notHiddenColumnIndex === null) {
      // There are no visible columns anymore, so move the selection out of the table edge. This will
      // be processed by the selection Transformer class as a move selection to the next row (if autoWrapRow is enabled).
      delta.col = this.hot.view.countRenderableColumnsInRange(highlight.col, this.hot.countCols());
    } else {
      delta.col = Math.max(this.hot.view.countRenderableColumnsInRange(highlight.col, notHiddenColumnIndex) - 1, 1);
    }
  }
}
/**
 * The hook observes the column selection from the Selection API and modifies the column range to
 * ensure that the whole nested column will be covered.
 *
 * @param {CellCoords} from The coords object where the selection starts.
 * @param {CellCoords} to The coords object where the selection ends.
 * @param {CellCoords} highlight The coords object where the focus is.
 */
function _onBeforeSelectColumns(from, to, highlight) {
  const headerLevel = from.row;
  const startNodeData = this._getHeaderTreeNodeDataByCoords({
    row: headerLevel,
    col: from.col
  });
  const endNodeData = this._getHeaderTreeNodeDataByCoords({
    row: headerLevel,
    col: to.col
  });
  _classPrivateFieldSet(_recentlyHighlightCoords, this, highlight.clone());
  if (to.col < from.col) {
    // Column selection from right to left
    if (startNodeData) {
      from.col = startNodeData.columnIndex + startNodeData.origColspan - 1;
    }
    if (endNodeData) {
      to.col = endNodeData.columnIndex;
    }
  } else if (to.col >= from.col) {
    // Column selection from left to right or a single column selection
    if (startNodeData) {
      from.col = startNodeData.columnIndex;
    }
    if (endNodeData) {
      to.col = endNodeData.columnIndex + endNodeData.origColspan - 1;
    }
  }
}
/**
 * `afterGetColumnHeader` hook callback - prepares the header structure.
 *
 * @param {Array} renderersArray Array of renderers.
 */
function _onAfterGetColumnHeaderRenderers(renderersArray) {
  if (_classPrivateFieldGet(_stateManager, this).getLayersCount() > 0) {
    renderersArray.length = 0;
    for (let headerLayer = 0; headerLayer < _classPrivateFieldGet(_stateManager, this).getLayersCount(); headerLayer++) {
      renderersArray.push(this.headerRendererFactory(headerLayer));
    }
  }
}
/**
 * Make the renderer render the first nested column in its entirety.
 *
 * @param {object} calc Viewport column calculator.
 */
function _onAfterViewportColumnCalculatorOverride(calc) {
  const headerLayersCount = _classPrivateFieldGet(_stateManager, this).getLayersCount();
  let newStartColumn = calc.startColumn;
  let nonRenderable = !!headerLayersCount;
  for (let headerLayer = 0; headerLayer < headerLayersCount; headerLayer++) {
    const startColumn = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(headerLayer, calc.startColumn);
    const renderedStartColumn = this.hot.columnIndexMapper.getRenderableFromVisualIndex(startColumn);

    // If any of the headers for that column index is rendered, all of them should be rendered properly, see
    // comment below.
    if (startColumn >= 0) {
      nonRenderable = false;
    }

    // `renderedStartColumn` can be `null` if the leftmost columns are hidden. In that case -> ignore that header
    // level, as it should be handled by the "parent" header
    if (isNumeric(renderedStartColumn) && renderedStartColumn < calc.startColumn) {
      newStartColumn = renderedStartColumn;
      break;
    }
  }

  // If no headers for the provided column index are renderable, start rendering from the beginning of the upmost
  // header for that position.
  calc.startColumn = nonRenderable ? _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(0, newStartColumn).columnIndex : newStartColumn;
}
/**
 * `modifyColWidth` hook callback - returns width from cache, when is greater than incoming from hook.
 *
 * @param {number} width Width from hook.
 * @param {number} column Visual index of an column.
 * @returns {number}
 */
function _onModifyColWidth(width, column) {
  const cachedWidth = this.ghostTable.getWidth(column);
  return width > cachedWidth ? width : cachedWidth;
}
/**
 * Listens the `modifyColumnHeaderValue` hook that overwrites the column headers values based on
 * the internal state and settings of the plugin.
 *
 * @param {string} value The column header value.
 * @param {number} visualColumnIndex The visual column index.
 * @param {number} headerLevel The index of header level. The header level accepts positive (0 to N)
 *                             and negative (-1 to -N) values. For positive values, 0 points to the
 *                             top most header, and for negative direction, -1 points to the most bottom
 *                             header (the header closest to the cells).
 * @returns {string} Returns the column header value to update.
 */
function _onModifyColumnHeaderValue(value, visualColumnIndex, headerLevel) {
  var _classPrivateFieldGet6;
  const {
    label
  } = (_classPrivateFieldGet6 = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet6 !== void 0 ? _classPrivateFieldGet6 : {
    label: ''
  };
  return label;
}
/**
 * `modifyFocusedElement` hook callback.
 *
 * @param {number} row Row index.
 * @param {number} column Column index.
 * @returns {HTMLTableCellElement} The `TH` element to be focused.
 */
function _onModifyFocusedElement(row, column) {
  if (row < 0) {
    return this.hot.getCell(row, _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(row, column), true);
  }
}
/**
 * Updates the plugin state after HoT initialization.
 */
function _onInit() {
  // @TODO: Workaround for broken plugin initialization abstraction.
  this.updatePlugin();
}
/**
 * Updates the plugin state after new dataset load.
 *
 * @param {Array[]} sourceData Array of arrays or array of objects containing data.
 * @param {boolean} initialLoad Flag that determines whether the data has been loaded
 *                              during the initialization.
 */
function _onAfterLoadData(sourceData, initialLoad) {
  if (!initialLoad) {
    this.updatePlugin();
  }
}